Skip to content

feat: add SCIM 2.0 user provisioning (EE)#1306

Open
brendan-kellam wants to merge 28 commits into
mainfrom
brendan/scim-user-provisioning
Open

feat: add SCIM 2.0 user provisioning (EE)#1306
brendan-kellam wants to merge 28 commits into
mainfrom
brendan/scim-user-provisioning

Conversation

@brendan-kellam

@brendan-kellam brendan-kellam commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Adds a SCIM 2.0 server (EE, gated by the new scim entitlement) so an identity provider (Okta, Entra) can provision and deprovision Sourcebot members.

Scope

  • /scim/v2 Users endpoints: discovery, list+filter, create, get, replace, PATCH (active toggle), delete. Groups deferred.
  • Deprovisioning soft-deactivates the membership (new UserToOrg.isActive): forces logout via sessionVersion and revokes API/OAuth tokens, but preserves the row so the IdP can reactivate.
  • Org-scoped ScimToken (bearer auth via withScimAuth); managed under Settings → Security.
  • JIT auto-join is suppressed once SCIM is enabled (IdP is the source of truth).

Note: the scim entitlement must be added to the lighthouse entitlements list and deployed before online licenses will grant it.

image image image image

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added SCIM provisioning for eligible organizations, including a Security settings area for enabling/disabling SCIM and managing provisioning tokens.
    • Implemented SCIM 2.0 API endpoints and a cleaner public /scim/v2 route.
    • Expanded membership management with suspended/SCIM-managed states, plus a unified Members table with search, filters, and row actions.
  • Bug Fixes

    • Updated onboarding, invite, and membership access flows to avoid join/redeem experiences when SCIM is enabled.
    • Refined seat usage and activity metrics to count active members and track member activation accurately.

Adds a SCIM 2.0 server so an IdP (Okta, Entra) can provision, update, and
deprovision org members. Users-only scope; deprovisioning soft-deactivates the
membership (forces logout + revokes tokens) rather than deleting it, and JIT
auto-join is suppressed when SCIM is enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds SCIM 2.0 endpoints, token management, and SCIM-aware membership state. It also splits membership actions into feature modules, updates login and join flows, and changes settings, billing, chat, and API responses to use active or suspended membership data.

Changes

SCIM provisioning and membership architecture update

Layer / File(s) Summary
Data model and shared SCIM primitives
packages/db/prisma/migrations/20260619214548_add_scim_users_support/migration.sql, packages/db/prisma/migrations/20260624194710_add_lastactiveat_to_usertoorg/migration.sql, packages/db/prisma/schema.prisma, packages/shared/src/constants.ts, packages/shared/src/crypto.ts, packages/shared/src/entitlements.ts, packages/shared/src/index.server.ts, packages/web/src/lib/errorCodes.ts, packages/web/src/lib/posthogEvents.ts, packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Org, UserToOrg, and ScimToken gain SCIM fields and indexes, and shared constants, errors, and exports add SCIM token and entitlement support.
SCIM protocol contracts and auth wrapper
packages/web/src/ee/features/scim/*, packages/web/src/ee/features/audit/types.ts
SCIM request schemas, response mappers, discovery documents, audit types, and bearer-token auth helpers define the SCIM wire contract.
SCIM settings actions and security UI
packages/web/src/app/(app)/settings/security/*, packages/web/src/app/(app)/settings/components/settingsCard.tsx, packages/web/src/features/scim/utils.ts, packages/web/src/features/membership/components/managedByScim*.tsx
The security settings page now toggles SCIM enablement, manages tokens, and shows SCIM-managed badges and disabled controls on related settings cards.
SCIM 2.0 API routes and rewrite
packages/web/next.config.mjs, packages/web/src/app/api/(server)/ee/scim/v2/*
The /scim/v2 rewrite and SCIM discovery/user routes authenticate tokens and serve SCIM resource and provisioning responses.
Membership service and auth gating
packages/web/src/features/membership/{utils.ts,errors.ts,logger.ts,membership.service.ts,membership.service.test.ts}, packages/web/src/middleware/{withAuth.ts,withAuth.test.ts}
Membership lifecycle helpers now track suspension, owner limits, seat availability, and activity timestamps, and auth middleware/tests treat suspended memberships as unauthenticated.
Membership actions and onboarding hooks
packages/web/src/actions.ts, packages/web/src/features/membership/actions/*, packages/web/src/features/membership/onCreateUser.ts, packages/web/src/auth.ts, packages/web/src/ee/features/membership/actions.ts, packages/web/src/ee/features/sso/sso.ts
Invite, account-request, and role-change actions move into membership feature modules, and onboarding/auth imports switch to the relocated hook.
Non-member app gating and request UX
packages/web/src/app/(app)/layout.tsx, packages/web/src/app/invite/page.tsx, packages/web/src/app/redeem/page.tsx, packages/web/src/app/components/logoutEscapeHatch.tsx, packages/web/src/app/redeem/components/acceptInviteCard.tsx, packages/web/src/features/membership/components/{joinOrganizationCard.tsx,submitJoinRequestCard.tsx,notProvisionedCard.tsx,pendingApprovalCard.tsx}
App-level join, redeem, and non-member flows branch on SCIM enablement and unsuspended membership state, with client-side join and logout interactions.
Settings layout shell
packages/web/src/app/(app)/settings/{accountAskAgent,analytics,apiKeys,connections,general,license,linked-accounts,mcp,workspaceAskAgent}/layout.tsx, packages/web/src/app/(app)/settings/components/settingsContainer.tsx, packages/web/src/app/(app)/settings/layout.tsx, packages/web/src/app/(app)/settings/members/layout.tsx
Shared settings containers and route layouts render settings sections through a common wrapper.
Members settings page and table UI
packages/web/src/app/(app)/settings/members/page.tsx, packages/web/src/app/(app)/settings/members/members*.tsx, packages/web/src/app/(app)/settings/members/components/invitesList.tsx, packages/web/src/app/(app)/settings/members/components/requestsList.tsx, packages/web/src/components/ui/table.tsx, packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx
The members settings page, table, filter, and action menu are rewritten around membership actions and per-row SCIM or member state.
Active membership filters across app and reporting
packages/web/src/app/onboard/page.tsx, packages/web/src/features/billing/actions.ts, packages/web/src/features/billing/servicePing.ts, packages/web/src/features/billing/types.ts, packages/web/src/features/chat/actions.ts, packages/web/src/app/api/(server)/ee/askmcp/callback/route.ts, packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts, packages/web/src/app/api/(server)/ee/users/route.ts, packages/web/src/openapi/publicApiSchemas.ts, docs/api-reference/sourcebot-public.openapi.json
Active and unsuspended membership filters now drive billing, chat, MCP access, user list output, and API or docs schemas.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • sourcebot-dev/sourcebot#988: The main PR is related to PR #988 because it introduces the same promoteToOwner / demoteToMember membership actions and removes the older user-management implementation.
  • sourcebot-dev/sourcebot#1069: Both PRs change packages/shared/src/entitlements.ts and the exported Entitlement union, so they touch the same entitlement surface.
  • sourcebot-dev/sourcebot#1097: The sidebar now imports getOrgAccountRequests from the membership actions module, which is directly tied to the sidebar wiring introduced in this PR.
  • sourcebot-dev/sourcebot#1168: The membership service now revokes credentials and removes org members with the same lifecycle semantics changed in this PR.

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding EE SCIM 2.0 user provisioning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/scim-user-provisioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/db/prisma/schema.prisma Outdated
@brendan-kellam brendan-kellam marked this pull request as ready for review June 20, 2026 21:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/app/invite/page.tsx (1)

45-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not treat inactive memberships as active members in redirect logic.

At Line 46, if (membership) redirects users with SCIM-deactivated rows (isActive: false) as if they were active members. This diverges from the auth contract where only active memberships confer access.

Suggested fix
-    if (membership) {
+    if (membership?.isActive) {
         redirect(`/`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/invite/page.tsx` around lines 45 - 47, The redirect
logic in the invite page only checks for the existence of a membership object,
but does not verify that it is active. Modify the condition in the if statement
that checks `membership` to also verify that the membership's active status is
true (check the isActive property). This ensures that users with deactivated
SCIM memberships are not incorrectly treated as active members and redirected,
maintaining consistency with the authentication contract.
🟡 Minor comments (8)
packages/web/src/app/(app)/settings/security/components/scimProvisioningSettings.tsx-125-127 (1)

125-127: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add accessible labels to icon-only action buttons.

Lines 125, 170, and 230 render icon-only buttons without accessible names, which makes these actions ambiguous for screen-reader users.

Also applies to: 170-172, 230-236

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/web/src/app/`(app)/settings/security/components/scimProvisioningSettings.tsx
around lines 125 - 127, The icon-only action buttons in the
scimProvisioningSettings component lack accessible labels for screen readers.
Add aria-label attributes to the Button components at handleCopyBaseUrl (around
line 125-127), the button around line 170-172, and the button around line
230-236 to provide descriptive accessible names that explain what each button
does when activated by assistive technology users.
packages/web/src/features/membership/components/joinOrganizationCard.tsx-56-63 (1)

56-63: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace CSS variable color classes with Tailwind semantic color classes.

text-[var(--muted-foreground)] and text-[var(--primary-foreground)] should use direct Tailwind classes in this TSX file.

Suggested fix
-                        <p className="text-[var(--muted-foreground)] text-[15px] leading-6">
+                        <p className="text-muted-foreground text-[15px] leading-6">
                             Welcome to Sourcebot! Click the button below to join this organization.
                         </p>
@@
-                        className="w-full h-11 bg-primary hover:bg-primary/90 text-[var(--primary-foreground)] transition-all duration-200 font-medium"
+                        className="w-full h-11 bg-primary hover:bg-primary/90 text-primary-foreground transition-all duration-200 font-medium"

As per coding guidelines, **/*.{tsx,jsx,mdx} must use Tailwind color classes directly instead of CSS variable syntax.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/features/membership/components/joinOrganizationCard.tsx`
around lines 56 - 63, In the joinOrganizationCard component, replace the CSS
variable syntax in the className attributes with direct Tailwind semantic color
classes. Change `text-[var(--muted-foreground)]` to `text-muted-foreground` in
the paragraph element and change `text-[var(--primary-foreground)]` to
`text-primary-foreground` in the Button component's className to align with the
coding guidelines that require TSX files to use Tailwind color classes directly
instead of CSS variable syntax.

Source: Coding guidelines

packages/web/src/features/membership/components/submitJoinRequestCard.tsx-60-70 (1)

60-70: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use Tailwind color tokens instead of CSS-variable utility syntax.

Replace text-[var(--...)] with token classes (text-primary-foreground, text-foreground, text-muted-foreground) for consistency with repo standards.

As per coding guidelines, **/*.{tsx,jsx,mdx}: Use Tailwind color classes directly instead of CSS variable syntax (e.g., use border-border instead of border-[var(--border)]).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/features/membership/components/submitJoinRequestCard.tsx`
around lines 60 - 70, Replace all CSS variable utility syntax with Tailwind
color token classes in the submitJoinRequestCard component. Specifically, update
the className attributes in the svg element and the h1 and p elements to use
text-primary-foreground, text-foreground, and text-muted-foreground instead of
text-[var(--primary-foreground)], text-[var(--foreground)], and
text-[var(--muted-foreground)] respectively. This aligns with the repository's
coding standards for consistent use of Tailwind color classes rather than CSS
variable syntax.

Source: Coding guidelines

packages/web/src/app/(app)/settings/members/components/membersList.tsx-217-217 (1)

217-217: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a fallback label when name is missing.

Line 217 renders member.name directly, so members without a profile name show a blank primary label. Use email fallback to keep identity visible.

Suggested fix
-                                                <span className="font-medium truncate">{member.name}</span>
+                                                <span className="font-medium truncate">{member.name ?? member.email}</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/`(app)/settings/members/components/membersList.tsx at
line 217, The span element rendering member.name on line 217 of membersList.tsx
displays blank when the name property is missing or empty, making member
identification difficult. Modify the content of the span to use a fallback
expression that displays member.email when member.name is not available, such as
using a logical OR operator or conditional expression to ensure every member row
has a visible identifier.
packages/web/src/app/api/(server)/ee/scim/v2/Users/[id]/route.ts-74-75 (1)

74-75: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unsafe non-null assertions after reload.

A concurrent delete between update and reload can make refreshed null; refreshed! then produces a 500 instead of a SCIM 404.

Suggested fix
-        const refreshed = await loadMembership(prisma, org.id, id);
-        return scimJson(toScimUser(refreshed!), 200);
+        const refreshed = await loadMembership(prisma, org.id, id);
+        if (!refreshed) {
+            return scimError(404, `User ${id} not found`);
+        }
+        return scimJson(toScimUser(refreshed), 200);

Also applies to: 113-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/api/`(server)/ee/scim/v2/Users/[id]/route.ts around
lines 74 - 75, The non-null assertion on the `refreshed` variable returned from
loadMembership can cause a 500 error if a concurrent delete occurs between the
update and reload, when it should return a SCIM 404 instead. Remove the non-null
assertion operator (!) after `refreshed` in the return statement at the scimJson
call, and add a null check immediately after the loadMembership assignment. If
refreshed is null, return an appropriate SCIM 404 response, otherwise proceed
with toScimUser(refreshed) only when the value is confirmed to be non-null.
Apply this same fix at the other location mentioned in the comment (lines
113-114).
packages/web/src/ee/features/scim/withScimAuth.ts-26-34 (1)

26-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Accept case-insensitive bearer auth schemes.

The current check rejects bearer/mixed-case schemes, which can break otherwise valid IdP requests.

Suggested fix
-    const authorization = request.headers.get("Authorization") ?? undefined;
-    if (!authorization?.startsWith("Bearer ")) {
+    const authorization = request.headers.get("Authorization") ?? undefined;
+    const [scheme, bearer] = authorization?.split(/\s+/, 2) ?? [];
+    if (scheme?.toLowerCase() !== "bearer" || !bearer) {
         return scimError(401, "Missing or malformed Authorization header");
     }
-
-    const bearer = authorization.slice("Bearer ".length);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/ee/features/scim/withScimAuth.ts` around lines 26 - 34, The
authorization header check for the Bearer token scheme is case-sensitive and
only accepts "Bearer " with that exact capitalization, which will reject valid
IdP requests that use lowercase "bearer" or mixed case variants. Convert the
authorization header value to lowercase before checking if it starts with
"Bearer " (or check against "bearer " in lowercase), and adjust the subsequent
slice operation that extracts the bearer token to use the lowercased
authorization value to ensure the token extraction works correctly regardless of
the case used in the Authorization header.
CHANGELOG.md-42-42 (1)

42-42: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Place this entry under [Unreleased] instead of a released version block.

Line 42 is currently under ## [5.0.3]. The project changelog policy requires PR entries to be added under [Unreleased] until release cut.

As per coding guidelines, CHANGELOG.md: “Update CHANGELOG.md with an entry under [Unreleased] linking to the new PR.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 42, The SCIM 2.0 server provisioning entry is currently
placed under the [5.0.3] released version section in CHANGELOG.md. Move this
entry from the [5.0.3] section to the [Unreleased] section at the top of the
changelog. The entry should maintain its exact format and content, just
relocated to comply with the project's changelog policy that requires new PR
entries to be added under [Unreleased] until the next release is cut.

Source: Coding guidelines

packages/web/src/ee/features/scim/actions.ts-73-85 (1)

73-85: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize and validate token names on the server boundary.

Line 73 and Line 115 accept name as-is. Without trimming/non-empty validation, you can create hard-to-manage tokens (e.g., whitespace-only or visually duplicate names with trailing spaces).

Proposed fix
 export const generateScimToken = async (name: string): Promise<{ token: string } | ServiceError> => sew(() =>
     withAuth(async ({ org, user, role, prisma }) =>
         withMinimumOrgRole(role, OrgRole.OWNER, async () => {
+            const normalizedName = name.trim();
+            if (!normalizedName) {
+                return {
+                    statusCode: StatusCodes.BAD_REQUEST,
+                    errorCode: ErrorCode.INVALID_REQUEST,
+                    message: "SCIM token name cannot be empty",
+                } satisfies ServiceError;
+            }
             if (!await hasEntitlement('scim')) {
                 return scimNotAvailable();
             }

             const existing = await prisma.scimToken.findFirst({
                 where: {
                     orgId: org.id,
-                    name,
+                    name: normalizedName,
                 },
             });
@@
             const scimToken = await prisma.scimToken.create({
                 data: {
-                    name,
+                    name: normalizedName,
                     hash,
                     orgId: org.id,
                 },
             });
@@
-                metadata: { scim_token: name },
+                metadata: { scim_token: normalizedName },
             });

Also applies to: 115-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/ee/features/scim/actions.ts` around lines 73 - 85, The
generateScimToken function accepts the name parameter without normalization or
validation, allowing whitespace-only or duplicate names with trailing spaces to
be created. Add validation at the beginning of the function to trim the name
parameter and verify it is not empty after trimming, returning an appropriate
error if validation fails. Apply the same normalization and validation logic to
the other function around line 115 that also handles scim token names.
🧹 Nitpick comments (1)
packages/db/prisma/schema.prisma (1)

436-437: ⚡ Quick win

Remove redundant uniqueness on primary key hash.

hash is already unique because it is the model @id; keeping @unique is redundant and can lead to unnecessary duplicate index artifacts in migrations.

Suggested schema cleanup
-  hash String `@id` `@unique`
+  hash String `@id`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/prisma/schema.prisma` around lines 436 - 437, Remove the
redundant `@unique` annotation from the hash field definition in the Prisma
schema. Since hash is already defined as the primary key with `@id`, the `@unique`
annotation is unnecessary and can create duplicate indexes. Modify the hash
field to only include `@id` `@unique`, removing the `@unique` directive entirely so it
reads as hash String `@id`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/prisma/schema.prisma`:
- Around line 410-413: Replace the non-unique index annotation @@index([orgId,
scimExternalId]) with a unique constraint @@unique([orgId, scimExternalId]) on
the scimExternalId field to enforce per-organization uniqueness of SCIM external
identifiers. This prevents duplicate IdP identities within the same org and
ensures unambiguous identity resolution in SCIM flows.

In `@packages/web/src/app/`(app)/layout.tsx:
- Around line 81-87: The gate logic currently only checks for the absence of a
membership but does not account for inactive memberships. When a membership row
exists with `isActive: false`, execution falls through and later assigns a role
from that inactive membership. Modify the membership validation check to ensure
that inactive memberships are treated the same as non-existent memberships by
adding a check for the `isActive` property. Update all conditions that check for
`!membership` to also verify `membership.isActive` is true, so that both missing
and inactive memberships are handled consistently as non-members in this gate.

In
`@packages/web/src/app/`(app)/settings/security/components/scimProvisioningSettings.tsx:
- Around line 102-110: The handleRevokeToken function calls the async
revokeScimToken action without a try/catch block, which means any exceptions
thrown by that function will escape unhandled and the user won't see an error
toast. Wrap the await revokeScimToken(name) call in a try/catch block, and in
the catch clause, display a destructive toast with an appropriate error message
to ensure all failure scenarios (both service errors and thrown exceptions) are
properly communicated to the user.

In `@packages/web/src/app/`(app)/settings/security/page.tsx:
- Around line 32-33: The SCIM token fetch logic in the security page converts
ServiceError results into an empty array, which masks retrieval failures as "no
tokens exist" in the UI. Instead of using the conditional assignment that checks
isServiceError(scimTokensResult) and defaults to an empty array, you need to
handle the error case separately so that the UI can distinguish between a
successful fetch with zero tokens versus a failed fetch. Create separate state
or variables to track both the scimTokens array and whether an error occurred
during the fetch, allowing the downstream UI to appropriately display either an
empty token list or an error message.

In `@packages/web/src/app/api/`(server)/ee/scim/v2/Users/[id]/route.ts:
- Around line 62-75: The code currently updates the user profile via
prisma.user.update() before validating the membership state change via
applyActive(), which can lead to partial updates if the membership change fails.
Reorder the operations so that applyActive() is called before
prisma.user.update() to validate the membership state change constraints first,
or alternatively wrap both the prisma.user.update() and applyActive() calls in a
database transaction to ensure that either both operations succeed or both fail
atomically. Apply the same fix to the similar code block mentioned at lines
98-115.
- Around line 64-67: The prisma.user.update() calls in both the PUT handler
(lines 64-67) and PATCH handler (lines 99-105) lack error handling for unique
email constraint violations. Wrap both prisma.user.update() calls in try-catch
blocks and catch errors related to unique constraint violations on the email
field. When such an error occurs, return a SCIM 409 Conflict response with
scimType set to "uniqueness" to match the existing error handling pattern
already implemented in the POST handler. Reference the POST handler's error
handling approach as a template for the proper response format.

In `@packages/web/src/app/api/`(server)/ee/scim/v2/Users/route.ts:
- Around line 21-25: The GET request handler is manually parsing query
parameters using parseInt, Math.max, and Math.min instead of using a Zod schema
for validation. Create a Zod schema that defines the shape and validation rules
for the query parameters (filter, startIndex, and count), including type
coercion and default values. Replace the manual parsing logic starting with the
params variable assignment through the count variable assignment with a single
Zod schema parse operation that validates and transforms the incoming
request.nextUrl.searchParams data, ensuring all validation logic is centralized
in the schema definition rather than scattered throughout the handler code.
- Around line 66-69: The current findUnique followed by create pattern for user
provisioning has a race condition where concurrent requests can both pass the
existence check before either executes the create, causing a unique constraint
violation on the email field. Replace the separate findUnique and create calls
with a single prisma.user.upsert operation that atomically checks for existence
and creates the user in one transaction, using email as the unique identifier
and providing both the create and update data objects with the email and name
fields.

In `@packages/web/src/app/invite/page.tsx`:
- Around line 54-55: The JoinOrganizationCard component is being rendered
without passing the validated inviteLinkId as a prop, which causes the
joinOrganization function to fail invite-link validation for organizations that
require member approval. Pass the inviteLinkId that should be available in the
page context as a prop to the JoinOrganizationCard component so it can be used
for proper validation when the user attempts to join the organization.

In `@packages/web/src/app/redeem/page.tsx`:
- Around line 45-48: The membership redirect check at the `if (membership)`
condition is catching both active and inactive memberships, including
SCIM-deactivated ones where isActive is false. This prevents the intended
not-provisioned flow from executing. Modify the condition to check not only that
membership exists but also that membership.isActive is true before redirecting
to the home page, ensuring only active members are redirected while deactivated
members proceed through the redemption flow.

In `@packages/web/src/ee/features/scim/actions.ts`:
- Around line 24-31: The getScimBaseUrl function and the other read operations
at lines 37-44 and 152-169 are implemented as server actions (wrapped with sew,
withAuth, withMinimumOrgRole), but they only perform data fetching without
mutations, which violates the project guideline that server actions should only
be used for POST/PUT/DELETE operations. Extract these read operations into
separate utility functions that return the data directly without server action
wrappers, then import and call them as regular async functions from client
components or other modules. Keep the authorization and entitlement checks, but
move them outside the server action pattern.
- Around line 80-103: Add a database-level unique constraint to prevent race
conditions in the SCIM token creation logic. In the ScimToken model located in
packages/db/prisma/schema.prisma, add a composite unique constraint using the
@@unique directive that combines the orgId and name fields. Then create a new
Prisma migration to apply this database constraint, which will ensure that the
pre-check validation in the create action is backed by database enforcement and
prevent concurrent requests from creating duplicate tokens with the same name
within the same organization.

In `@packages/web/src/ee/features/scim/schemas.ts`:
- Around line 22-30: The scimUserCreateSchema currently accepts emails through
the emails array without validating that they are actual email addresses before
they are persisted as user.email in downstream handlers. Update the
scimEmailSchema definition to include proper email format validation (ensure the
email field uses z.string().email() or equivalent). Additionally, locate the
other email validation location mentioned around line 65-66 and apply the same
email format validation to prevent blank or non-email identifiers from being
stored in the database.

In `@packages/web/src/features/membership/actions/accountRequests.ts`:
- Around line 65-125: Remove the redundant if (!existingRequest) conditional
check in the createAccountRequest function since an early return at line 57-63
already handles the case when existingRequest is truthy. Unindent all code that
was nested inside this conditional block by one level and remove the closing
brace at line 125. Additionally, in the approveAccountRequest function, add
deletion of the accountRequest record after successfully calling addMember
(around line 181) to match the cleanup pattern used in rejectAccountRequest,
ensuring that approved account requests do not leave orphaned records in the
database.

In `@packages/web/src/features/membership/components/submitJoinRequestCard.tsx`:
- Around line 18-45: The handleSubmit function in submitJoinRequestCard.tsx does
not have proper error handling for exceptions thrown by createAccountRequest().
If createAccountRequest() throws an error, setIsSubmitting(false) will never
execute, leaving the button locked. Refactor the handleSubmit function to wrap
the createAccountRequest() call and subsequent logic in a try/catch/finally
block, placing the setIsSubmitting(false) call in the finally block to ensure it
always executes. In the catch block, display an error toast with the caught
error message so users are informed of any unexpected failures.

In `@packages/web/src/features/membership/membership.service.ts`:
- Around line 47-49: The seat capacity check using orgHasAvailability() is
performed outside the database transaction in both the membership add and
reactivation code paths, allowing concurrent requests to bypass the seat limit.
Move the orgHasAvailability() check inside the same database transaction that
performs the actual insert or reactivate operation for the membership record,
ensuring the capacity validation and data persistence happen atomically
together. Apply this transactional pattern consistently to both the add path
(around the seatLimitReached() check) and the reactivation path to prevent race
conditions where multiple concurrent requests can each pass the check separately
and then both persist, exceeding the seat cap.

In `@packages/web/src/features/membership/onCreateUser.ts`:
- Around line 43-50: The first-user OWNER role assignment in the onCreateUser
function is prone to race conditions because it uses a non-atomic read of the
members array to determine if this is the first user. Replace the current
approach where __unsafePrisma.org.findUnique loads the full members array and
members.length is checked at line 76-81 with a single atomic transaction in the
membership layer that performs both a count query (instead of loading full
members) and the membership creation in one serializable unit. This ensures
concurrent user creations cannot both observe empty membership and both assign
OrgRole.OWNER.

---

Outside diff comments:
In `@packages/web/src/app/invite/page.tsx`:
- Around line 45-47: The redirect logic in the invite page only checks for the
existence of a membership object, but does not verify that it is active. Modify
the condition in the if statement that checks `membership` to also verify that
the membership's active status is true (check the isActive property). This
ensures that users with deactivated SCIM memberships are not incorrectly treated
as active members and redirected, maintaining consistency with the
authentication contract.

---

Minor comments:
In `@CHANGELOG.md`:
- Line 42: The SCIM 2.0 server provisioning entry is currently placed under the
[5.0.3] released version section in CHANGELOG.md. Move this entry from the
[5.0.3] section to the [Unreleased] section at the top of the changelog. The
entry should maintain its exact format and content, just relocated to comply
with the project's changelog policy that requires new PR entries to be added
under [Unreleased] until the next release is cut.

In `@packages/web/src/app/`(app)/settings/members/components/membersList.tsx:
- Line 217: The span element rendering member.name on line 217 of
membersList.tsx displays blank when the name property is missing or empty,
making member identification difficult. Modify the content of the span to use a
fallback expression that displays member.email when member.name is not
available, such as using a logical OR operator or conditional expression to
ensure every member row has a visible identifier.

In
`@packages/web/src/app/`(app)/settings/security/components/scimProvisioningSettings.tsx:
- Around line 125-127: The icon-only action buttons in the
scimProvisioningSettings component lack accessible labels for screen readers.
Add aria-label attributes to the Button components at handleCopyBaseUrl (around
line 125-127), the button around line 170-172, and the button around line
230-236 to provide descriptive accessible names that explain what each button
does when activated by assistive technology users.

In `@packages/web/src/app/api/`(server)/ee/scim/v2/Users/[id]/route.ts:
- Around line 74-75: The non-null assertion on the `refreshed` variable returned
from loadMembership can cause a 500 error if a concurrent delete occurs between
the update and reload, when it should return a SCIM 404 instead. Remove the
non-null assertion operator (!) after `refreshed` in the return statement at the
scimJson call, and add a null check immediately after the loadMembership
assignment. If refreshed is null, return an appropriate SCIM 404 response,
otherwise proceed with toScimUser(refreshed) only when the value is confirmed to
be non-null. Apply this same fix at the other location mentioned in the comment
(lines 113-114).

In `@packages/web/src/ee/features/scim/actions.ts`:
- Around line 73-85: The generateScimToken function accepts the name parameter
without normalization or validation, allowing whitespace-only or duplicate names
with trailing spaces to be created. Add validation at the beginning of the
function to trim the name parameter and verify it is not empty after trimming,
returning an appropriate error if validation fails. Apply the same normalization
and validation logic to the other function around line 115 that also handles
scim token names.

In `@packages/web/src/ee/features/scim/withScimAuth.ts`:
- Around line 26-34: The authorization header check for the Bearer token scheme
is case-sensitive and only accepts "Bearer " with that exact capitalization,
which will reject valid IdP requests that use lowercase "bearer" or mixed case
variants. Convert the authorization header value to lowercase before checking if
it starts with "Bearer " (or check against "bearer " in lowercase), and adjust
the subsequent slice operation that extracts the bearer token to use the
lowercased authorization value to ensure the token extraction works correctly
regardless of the case used in the Authorization header.

In `@packages/web/src/features/membership/components/joinOrganizationCard.tsx`:
- Around line 56-63: In the joinOrganizationCard component, replace the CSS
variable syntax in the className attributes with direct Tailwind semantic color
classes. Change `text-[var(--muted-foreground)]` to `text-muted-foreground` in
the paragraph element and change `text-[var(--primary-foreground)]` to
`text-primary-foreground` in the Button component's className to align with the
coding guidelines that require TSX files to use Tailwind color classes directly
instead of CSS variable syntax.

In `@packages/web/src/features/membership/components/submitJoinRequestCard.tsx`:
- Around line 60-70: Replace all CSS variable utility syntax with Tailwind color
token classes in the submitJoinRequestCard component. Specifically, update the
className attributes in the svg element and the h1 and p elements to use
text-primary-foreground, text-foreground, and text-muted-foreground instead of
text-[var(--primary-foreground)], text-[var(--foreground)], and
text-[var(--muted-foreground)] respectively. This aligns with the repository's
coding standards for consistent use of Tailwind color classes rather than CSS
variable syntax.

---

Nitpick comments:
In `@packages/db/prisma/schema.prisma`:
- Around line 436-437: Remove the redundant `@unique` annotation from the hash
field definition in the Prisma schema. Since hash is already defined as the
primary key with `@id`, the `@unique` annotation is unnecessary and can create
duplicate indexes. Modify the hash field to only include `@id` `@unique`, removing
the `@unique` directive entirely so it reads as hash String `@id`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0afa80cc-c399-4e0f-9b31-b82624e66b85

📥 Commits

Reviewing files that changed from the base of the PR and between 26435a4 and 199c2bd.

📒 Files selected for processing (74)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260619214548_add_scim_users_support/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/shared/src/constants.ts
  • packages/shared/src/crypto.ts
  • packages/shared/src/entitlements.ts
  • packages/shared/src/index.server.ts
  • packages/web/next.config.mjs
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/actions.ts
  • packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx
  • packages/web/src/app/(app)/components/submitAccountRequestButton.tsx
  • packages/web/src/app/(app)/components/submitJoinRequest.tsx
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/settings/components/settingsCard.tsx
  • packages/web/src/app/(app)/settings/layout.tsx
  • packages/web/src/app/(app)/settings/members/components/inviteMemberCard.tsx
  • packages/web/src/app/(app)/settings/members/components/invitesList.tsx
  • packages/web/src/app/(app)/settings/members/components/membersList.tsx
  • packages/web/src/app/(app)/settings/members/components/requestsList.tsx
  • packages/web/src/app/(app)/settings/members/page.tsx
  • packages/web/src/app/(app)/settings/security/components/inviteLinkEnabledSettingsCard.tsx
  • packages/web/src/app/(app)/settings/security/components/memberApprovalRequiredSettingsCard.tsx
  • packages/web/src/app/(app)/settings/security/components/scimEnabledSettingsCard.tsx
  • packages/web/src/app/(app)/settings/security/components/scimProvisioningSettings.tsx
  • packages/web/src/app/(app)/settings/security/components/scimUpsellCard.tsx
  • packages/web/src/app/(app)/settings/security/page.tsx
  • packages/web/src/app/api/(server)/ee/scim/v2/ResourceTypes/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Schemas/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/ServiceProviderConfig/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/[id]/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/route.ts
  • packages/web/src/app/components/joinOrganizationButton.tsx
  • packages/web/src/app/components/joinOrganizationCard.tsx
  • packages/web/src/app/components/logoutEscapeHatch.tsx
  • packages/web/src/app/invite/actions.ts
  • packages/web/src/app/invite/page.tsx
  • packages/web/src/app/redeem/components/acceptInviteCard.tsx
  • packages/web/src/app/redeem/page.tsx
  • packages/web/src/auth.ts
  • packages/web/src/ee/features/audit/types.ts
  • packages/web/src/ee/features/membership/actions.ts
  • packages/web/src/ee/features/scim/actions.ts
  • packages/web/src/ee/features/scim/constants.ts
  • packages/web/src/ee/features/scim/mapper.ts
  • packages/web/src/ee/features/scim/schemas.test.ts
  • packages/web/src/ee/features/scim/schemas.ts
  • packages/web/src/ee/features/scim/withScimAuth.ts
  • packages/web/src/ee/features/sso/sso.ts
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/features/membership/actions/accountRequests.ts
  • packages/web/src/features/membership/actions/index.ts
  • packages/web/src/features/membership/actions/invites.ts
  • packages/web/src/features/membership/actions/members.ts
  • packages/web/src/features/membership/components/deactivatedMemberBadge.tsx
  • packages/web/src/features/membership/components/joinOrganizationCard.tsx
  • packages/web/src/features/membership/components/managedByScimBadge.tsx
  • packages/web/src/features/membership/components/managedByScimNotice.tsx
  • packages/web/src/features/membership/components/notProvisionedCard.tsx
  • packages/web/src/features/membership/components/pendingApprovalCard.tsx
  • packages/web/src/features/membership/components/submitJoinRequestCard.tsx
  • packages/web/src/features/membership/errors.ts
  • packages/web/src/features/membership/logger.ts
  • packages/web/src/features/membership/membership.service.test.ts
  • packages/web/src/features/membership/membership.service.ts
  • packages/web/src/features/membership/onCreateUser.ts
  • packages/web/src/features/membership/utils.ts
  • packages/web/src/features/scim/utils.ts
  • packages/web/src/features/userManagement/actions.ts
  • packages/web/src/lib/authUtils.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts
  • packages/web/src/middleware/withAuth.test.ts
  • packages/web/src/middleware/withAuth.ts
💤 Files with no reviewable changes (8)
  • packages/web/src/app/(app)/components/submitAccountRequestButton.tsx
  • packages/web/src/app/(app)/components/submitJoinRequest.tsx
  • packages/web/src/ee/features/userManagement/actions.ts
  • packages/web/src/features/userManagement/actions.ts
  • packages/web/src/app/components/joinOrganizationCard.tsx
  • packages/web/src/app/components/joinOrganizationButton.tsx
  • packages/web/src/app/invite/actions.ts
  • packages/web/src/lib/authUtils.ts

Comment thread packages/db/prisma/schema.prisma Outdated
Comment thread packages/web/src/app/(app)/layout.tsx
Comment thread packages/web/src/app/(app)/settings/security/page.tsx Outdated
Comment thread packages/web/src/app/api/(server)/ee/scim/v2/Users/[id]/route.ts
Comment thread packages/web/src/ee/features/scim/schemas.ts
Comment thread packages/web/src/features/membership/actions/accountRequests.ts Outdated
Comment thread packages/web/src/features/membership/membership.service.ts Outdated
Comment thread packages/web/src/features/membership/onCreateUser.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/features/membership/membership.service.ts`:
- Around line 40-47: In the ensureActiveMember function, when an existing active
membership is found (the first if condition checking existing &&
existing.isActive), add a check to see if the incoming scimExternalId differs
from the existing membership's scimExternalId. If they differ, delegate to
setMemberActive with the active flag set to true and the provided
scimExternalId, rather than returning the existing membership immediately. This
ensures the SCIM external ID is persisted consistently across all code paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f28d6b43-479d-4001-b746-c114dfaf7c40

📥 Commits

Reviewing files that changed from the base of the PR and between 199c2bd and 260b789.

📒 Files selected for processing (21)
  • docs/api-reference/sourcebot-public.openapi.json
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/settings/license/page.tsx
  • packages/web/src/app/(app)/settings/members/page.tsx
  • packages/web/src/app/api/(server)/ee/askmcp/callback/route.ts
  • packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/route.ts
  • packages/web/src/app/api/(server)/ee/users/route.ts
  • packages/web/src/app/invite/page.tsx
  • packages/web/src/app/onboard/page.tsx
  • packages/web/src/app/redeem/page.tsx
  • packages/web/src/features/billing/actions.ts
  • packages/web/src/features/billing/servicePing.ts
  • packages/web/src/features/chat/actions.ts
  • packages/web/src/features/membership/actions/accountRequests.ts
  • packages/web/src/features/membership/actions/invites.ts
  • packages/web/src/features/membership/membership.service.test.ts
  • packages/web/src/features/membership/membership.service.ts
  • packages/web/src/features/membership/onCreateUser.ts
  • packages/web/src/features/membership/utils.ts
  • packages/web/src/openapi/publicApiSchemas.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/redeem/page.tsx
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/route.ts
  • packages/web/src/features/membership/onCreateUser.ts
  • packages/web/src/features/membership/actions/invites.ts
  • packages/web/src/features/membership/actions/accountRequests.ts

Comment thread packages/web/src/features/membership/membership.service.ts Outdated
@mintlify

mintlify Bot commented Jun 24, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
sourcebot 🟢 Ready View Preview Jun 24, 2026, 3:13 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

brendan-kellam and others added 3 commits June 24, 2026 16:37
…mbers

Add a per-membership lastActiveAt to UserToOrg, stamped in getAuthContext
alongside the existing global User.lastActiveAt. The migration backfills it
from User.lastActiveAt so existing members are not reset to "never active".

Switch the service ping's DAU/WAU/MAU and billed user count onto the per-org
timestamp. billedUserCount now counts only active (non-suspended) members who
have been active in the org at least once, so provisioned-but-never-signed-in
members no longer consume a seat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
packages/web/src/middleware/withAuth.test.ts (1)

486-552: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for suspended activity updates.

The suspended-member tests should also assert prisma.userToOrg.updateMany is not called, so the auth denial cannot still mark deprovisioned memberships active.

Also applies to: 930-986

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/middleware/withAuth.test.ts` around lines 486 - 552, The
suspended-member auth tests in withAuth.test.ts currently verify role denial but
do not cover the side effect on membership activity. Update the
suspended-membership cases in getAuthContext to also assert
prisma.userToOrg.updateMany is never called, including the API-key path, so
suspended or deprovisioned memberships cannot be marked active when access is
denied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/app/`(app)/settings/apiKeys/layout.tsx:
- Around line 5-12: The layout props type in authenticatedPage currently
references React.ReactNode without React in scope, which will fail type-checking
under the current JSX settings. Update the apiKeys layout to import type
ReactNode and use that type for the children prop in the authenticatedPage
generic, keeping the existing SettingsContainer and notFound logic unchanged.

In `@packages/web/src/app/`(app)/settings/members/membersTable.tsx:
- Around line 367-382: Biome is flagging the intentional trigger dependencies in
the two `useLayoutEffect` hooks inside `membersTable.tsx`. Keep the current
scroll restore/top behavior in `membersTable` by adding targeted Biome
dependency ignores for the `sorting` effect and the `filter, searchQuery`
effect, rather than removing those dependencies. Make the ignores as narrow as
possible and keep the rest of the hook logic unchanged.

In `@packages/web/src/app/`(app)/settings/members/membersTableActions.tsx:
- Around line 285-312: The promote/demote actions in membersTableActions and the
corresponding membership mutations in actions.ts are still available for
SCIM-managed orgs, unlike the other membership changes. Add the same SCIM guard
used elsewhere so promoteToOwner and demoteToMember are disabled or blocked when
the org is SCIM-managed, and make sure the server-side actions also enforce this
restriction rather than relying only on the UI.

In `@packages/web/src/app/api/`(server)/ee/users/route.ts:
- Line 59: The EE user-list public API in the route backed by
publicEeUserListItemSchema now exposes suspendedAt instead of isActive, which is
a breaking response-shape change for consumers. Update the route and any related
schema references so the exported user list contract is consistent, then
regenerate the OpenAPI output with the web workspace openapi generation task and
refresh docs/api-reference/sourcebot-public.openapi.json. Also add a brief
migration/release-notes entry calling out that isActive was replaced by
suspendedAt.

In `@packages/web/src/features/billing/servicePing.ts`:
- Around line 48-65: The DAU/WAU/MAU queries in servicePing still count
suspended memberships because they only filter by lastActiveAt; update the count
conditions to use the same active-membership criteria as activeUserCount, likely
by reusing activeMembershipWhere() or adding an equivalent suspendedAt exclusion
alongside the existing orgId and activity cutoff checks in the servicePing
metrics query.

In `@packages/web/src/features/membership/membership.service.test.ts`:
- Around line 20-23: Add activeMembershipWhere to the
`@/features/membership/utils` mock because the current vi.mock replacement omits
it, causing countActiveOwners to see it as undefined during removeMember and
setMemberRole tests. Update the membership.service.test.ts mock around the
membership utils override so it either exports activeMembershipWhere alongside
orgHasAvailability and unsuspendedMembershipWhere, or spreads the real module
and overrides only the needed helpers, keeping countActiveOwners functional.

In `@packages/web/src/middleware/withAuth.ts`:
- Around line 113-115: Skip updating membership activity when the membership is
suspended. In withAuth, before calling updateMembershipLastActiveAt(membership),
add a guard using the membership record so suspended rows are ignored and only
active memberships update UserToOrg.lastActiveAt. Keep the change localized to
the membership handling block in withAuth and use the existing membership object
to detect suspension.

---

Nitpick comments:
In `@packages/web/src/middleware/withAuth.test.ts`:
- Around line 486-552: The suspended-member auth tests in withAuth.test.ts
currently verify role denial but do not cover the side effect on membership
activity. Update the suspended-membership cases in getAuthContext to also assert
prisma.userToOrg.updateMany is never called, including the API-key path, so
suspended or deprovisioned memberships cannot be marked active when access is
denied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4af5ee55-42e0-4afc-879c-f28e8800efe1

📥 Commits

Reviewing files that changed from the base of the PR and between 2234f8d and 12355df.

📒 Files selected for processing (49)
  • docs/api-reference/sourcebot-public.openapi.json
  • packages/db/prisma/migrations/20260619214548_add_scim_users_support/migration.sql
  • packages/db/prisma/migrations/20260624194710_add_lastactiveat_to_usertoorg/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/settings/accountAskAgent/layout.tsx
  • packages/web/src/app/(app)/settings/analytics/layout.tsx
  • packages/web/src/app/(app)/settings/apiKeys/layout.tsx
  • packages/web/src/app/(app)/settings/components/settingsContainer.tsx
  • packages/web/src/app/(app)/settings/connections/layout.tsx
  • packages/web/src/app/(app)/settings/general/layout.tsx
  • packages/web/src/app/(app)/settings/layout.tsx
  • packages/web/src/app/(app)/settings/license/layout.tsx
  • packages/web/src/app/(app)/settings/license/page.tsx
  • packages/web/src/app/(app)/settings/linked-accounts/layout.tsx
  • packages/web/src/app/(app)/settings/mcp/layout.tsx
  • packages/web/src/app/(app)/settings/members/components/inviteMemberCard.tsx
  • packages/web/src/app/(app)/settings/members/components/invitesList.tsx
  • packages/web/src/app/(app)/settings/members/components/membersList.tsx
  • packages/web/src/app/(app)/settings/members/components/requestsList.tsx
  • packages/web/src/app/(app)/settings/members/layout.tsx
  • packages/web/src/app/(app)/settings/members/membersFilterSelect.tsx
  • packages/web/src/app/(app)/settings/members/membersTable.tsx
  • packages/web/src/app/(app)/settings/members/membersTableActions.tsx
  • packages/web/src/app/(app)/settings/members/membersTableView.tsx
  • packages/web/src/app/(app)/settings/members/page.tsx
  • packages/web/src/app/(app)/settings/security/layout.tsx
  • packages/web/src/app/(app)/settings/workspaceAskAgent/layout.tsx
  • packages/web/src/app/api/(server)/ee/askmcp/callback/route.ts
  • packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/[id]/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/route.ts
  • packages/web/src/app/api/(server)/ee/users/route.ts
  • packages/web/src/app/invite/page.tsx
  • packages/web/src/app/onboard/page.tsx
  • packages/web/src/app/redeem/page.tsx
  • packages/web/src/components/ui/table.tsx
  • packages/web/src/ee/features/scim/mapper.ts
  • packages/web/src/features/billing/actions.ts
  • packages/web/src/features/billing/servicePing.ts
  • packages/web/src/features/billing/types.ts
  • packages/web/src/features/chat/actions.ts
  • packages/web/src/features/membership/actions/members.ts
  • packages/web/src/features/membership/membership.service.test.ts
  • packages/web/src/features/membership/membership.service.ts
  • packages/web/src/features/membership/utils.ts
  • packages/web/src/middleware/withAuth.test.ts
  • packages/web/src/middleware/withAuth.ts
  • packages/web/src/openapi/publicApiSchemas.ts
💤 Files with no reviewable changes (4)
  • packages/web/src/app/(app)/settings/members/components/inviteMemberCard.tsx
  • packages/web/src/app/(app)/settings/members/components/membersList.tsx
  • packages/web/src/app/(app)/settings/members/components/requestsList.tsx
  • packages/web/src/app/(app)/settings/members/components/invitesList.tsx
✅ Files skipped from review due to trivial changes (5)
  • packages/web/src/app/(app)/settings/accountAskAgent/layout.tsx
  • packages/web/src/app/(app)/settings/security/layout.tsx
  • packages/web/src/app/(app)/settings/mcp/layout.tsx
  • packages/web/src/app/(app)/settings/linked-accounts/layout.tsx
  • packages/web/src/features/billing/types.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/web/src/app/(app)/settings/layout.tsx
  • packages/web/src/app/api/(server)/ee/askmcp/callback/route.ts
  • packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts
  • packages/web/src/app/onboard/page.tsx
  • packages/web/src/app/invite/page.tsx
  • packages/web/src/features/chat/actions.ts
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/settings/license/page.tsx
  • packages/web/src/ee/features/scim/mapper.ts
  • packages/web/src/app/redeem/page.tsx
  • packages/web/src/features/billing/actions.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/route.ts
  • packages/web/src/app/api/(server)/ee/scim/v2/Users/[id]/route.ts

Comment thread packages/web/src/app/(app)/settings/apiKeys/layout.tsx
Comment thread packages/web/src/app/(app)/settings/members/membersTable.tsx
Comment thread packages/web/src/app/(app)/settings/members/membersTableActions.tsx
Comment thread packages/web/src/app/api/(server)/ee/users/route.ts
Comment thread packages/web/src/features/billing/servicePing.ts
Comment thread packages/web/src/features/membership/membership.service.test.ts
Comment thread packages/web/src/middleware/withAuth.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/middleware/withAuth.ts (1)

119-130: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Re-read membership before granting access after pending activation.

role is computed from the pre-transaction row. If the guarded activation updates 0 rows because the membership was concurrently suspended/deleted, this still refreshes activity and returns the stale role. Have activatePendingMembership return the activated/current row or re-fetch the membership here before deriving role.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/middleware/withAuth.ts` around lines 119 - 130, The access
flow in withAuth is using a stale membership row after
activatePendingMembership, so role can be granted from pre-transaction data.
Update the withAuth path to re-read the membership after activation, or have
activatePendingMembership return the current activated row, and then derive role
only from that refreshed membership before calling updateMembershipLastActiveAt
or returning access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/web/src/middleware/withAuth.ts`:
- Around line 119-130: The access flow in withAuth is using a stale membership
row after activatePendingMembership, so role can be granted from pre-transaction
data. Update the withAuth path to re-read the membership after activation, or
have activatePendingMembership return the current activated row, and then derive
role only from that refreshed membership before calling
updateMembershipLastActiveAt or returning access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52a663a3-17e6-42c6-b2d4-099de06aa499

📥 Commits

Reviewing files that changed from the base of the PR and between 12355df and 1d3a2fd.

📒 Files selected for processing (6)
  • packages/web/src/app/error.tsx
  • packages/web/src/features/membership/components/orgAtCapacityCard.tsx
  • packages/web/src/features/membership/membership.service.test.ts
  • packages/web/src/features/membership/membership.service.ts
  • packages/web/src/middleware/withAuth.test.ts
  • packages/web/src/middleware/withAuth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/middleware/withAuth.test.ts

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

License Audit

Status: FAIL

Metric Count
Total packages 2221
Resolved (non-standard) 11
Unresolved 1
Strong copyleft 0
Weak copyleft 39

Fail Reasons

  • 1 package has an unresolvable license: element-source

Unresolved Packages

Package Version License Reason
element-source 0.0.3 UNKNOWN No license declared on the npm registry (no license field, no 'licenses' object), no repository or homepage URL in oss-licenses.json, and the package has no README or repository on the registry to inspect a LICENSE file.

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
dompurify 3.4.11 (MPL-2.0 OR Apache-2.0)
lightningcss 1.32.0 MPL-2.0
lightningcss-android-arm64 1.32.0 MPL-2.0
lightningcss-darwin-arm64 1.32.0 MPL-2.0
lightningcss-darwin-x64 1.32.0 MPL-2.0
lightningcss-freebsd-x64 1.32.0 MPL-2.0
lightningcss-linux-arm-gnueabihf 1.32.0 MPL-2.0
lightningcss-linux-arm64-gnu 1.32.0 MPL-2.0
lightningcss-linux-arm64-musl 1.32.0 MPL-2.0
lightningcss-linux-x64-gnu 1.32.0 MPL-2.0
lightningcss-linux-x64-musl 1.32.0 MPL-2.0
lightningcss-win32-arm64-msvc 1.32.0 MPL-2.0
lightningcss-win32-x64-msvc 1.32.0 MPL-2.0
Resolved Packages (11)
Package Version Original Resolved Source
@react-grab/cli 0.1.23 UNKNOWN MIT GitHub repo (aidenybai/react-grab LICENSE)
@react-grab/cli 0.1.29 UNKNOWN MIT GitHub repo (aidenybai/react-grab LICENSE)
@react-grab/mcp 0.1.29 UNKNOWN MIT GitHub repo (aidenybai/react-grab monorepo LICENSE)
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 npm registry (license field, livebook-dev repo)
khroma 2.1.0 UNKNOWN MIT GitHub repo (fabiospampinato/khroma license file)
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 npm registry (license field, livebook-dev repo)
map-stream 0.1.0 UNKNOWN MIT GitHub repo (dominictarr/map-stream LICENCE)
memorystream 0.3.1 UNKNOWN MIT extracted from npm 'licenses' object ([{type:'MIT'}])
pause-stream 0.0.11 ['MIT', 'Apache2'] MIT OR Apache-2.0 extracted from object/array license field
posthog-js 1.369.0 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo (PostHog/posthog-js LICENSE file)
valid-url 1.0.9 UNKNOWN MIT GitHub repo (ogt/valid-url LICENSE file)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants